import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

import javax.swing.JPanel;

public class Simulation extends JPanel implements Runnable, KeyListener {

	private Thread animator;

	public static final int WORLD_WIDTH = 800;
	public static final int WORLD_HEIGTH = 600;

	private boolean gameRunning;

	private final float BOUNCE = 0.75f;
	private static long SLEEP = 10;
	private static final int CHROMO_LENGTH = 5;
	private int popSize = 0;
	private float distanceEnemy = 6.0f;
	private float distanceMate = 3.0f;
	private float distanceEating = 3.0f;
	private int lowStrength = 70;

	private ArrayList<Creature> creatures;
	private ArrayList<Nutrient> nutrients;

	private int currentGeneration;

	public Simulation() {
		setBackground(Color.BLACK);
		setDoubleBuffered(true);
		setFocusable(true);
		setSize(WORLD_WIDTH, WORLD_HEIGTH);

		addKeyListener(this);

		currentGeneration = 0;
		gameRunning = true;
		popSize = (int) (10 + (Math.random() * ((20 - 10) + 1)));
		nutrients = new ArrayList<Nutrient>();
		creatures = new ArrayList<Creature>();
		for (int i = 0; i < popSize; i++) {
			Creature c = new Creature();
			creatures.add(c);
		}
	}

	public void addNotify() {
		super.addNotify();
		animator = new Thread(this);
		animator.start();
		gameRunning = true;

		currentGeneration = 0;
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);

		Graphics2D g2d = (Graphics2D) g;

		int xCoor;
		int yCoor;

		for (int i = 0; i < creatures.size(); i++) {
			xCoor = (int) creatures.get(i).getX();
			yCoor = (int) creatures.get(i).getY();
			g2d.setColor(Color.YELLOW);
			g2d.fillOval(xCoor, yCoor, 3, 3);
		}
		
		for (int i = 0; i < nutrients.size(); i++) {
			xCoor = (int) nutrients.get(i).getX();
			yCoor = (int) nutrients.get(i).getY();
			g2d.setColor(Color.RED);
			g2d.fillOval(xCoor, yCoor, 1, 1);
		}

		Toolkit.getDefaultToolkit().sync();
		g.dispose();
	}

	public Color getColorCreature(int type) {
		Color c;

		switch (type) {
		case 1:
			c = Color.GREEN;
			break;
		case 2:
			c = Color.RED;
			break;
		case 3:
			c = Color.BLUE;
			break;
		case 4:
			c = Color.ORANGE;
			break;
		case 5:
			c = Color.CYAN;
			break;
		default:
			c = null;
		}

		return c;
	}

	public void run() {
		
		while (creatures.size() > 0) {

			if (gameRunning) {
				currentGeneration++;

				validate();
				repaint();
				flock();
				repaint();

				Creature c1 = selection();
				Creature c2 = selection();

				// reproduce
				cross(c1, c2);
				cross(c2, c1);
				
			}

			try {
				Thread.sleep(SLEEP);
			} catch (InterruptedException e) {
				System.out.println("interrupted");
			}
		}
	}

	public String crossover(Creature c1, Creature c2) {

		// create a random crossover point
		int crossover = (int) (Math.random() * CHROMO_LENGTH);

		String t1 = c1.getChromosome().substring(0, crossover);
		String t2 = c2.getChromosome().substring(crossover);
		String s = t1.concat(t2);
		return s;

	}

	public void cross(Creature c1, Creature c2) {

		// create a random crossover point
		int crossover = (int) (Math.random() * CHROMO_LENGTH);

		Creature c = new Creature();
		if (crossover == 0) {
			c.setStrength(c1.getStrength());
			c.setDefense(c2.getDefense());
			c.setMates(c2);
			c.setEnemies(c2);
			c.setFood(c2);
		} else if (crossover == 1) {
			c.setStrength(c1.getStrength());
			c.setDefense(c1.getDefense());
			c.setMates(c2);
			c.setEnemies(c2);
			c.setFood(c2);
		} else if (crossover == 2) {
			c.setStrength(c1.getStrength());
			c.setDefense(c1.getDefense());
			c.setMates(c1);
			c.setEnemies(c2);
			c.setFood(c2);
		} else if (crossover == 3) {
			c.setStrength(c1.getStrength());
			c.setDefense(c1.getDefense());
			c.setMates(c1);
			c.setEnemies(c1);
			c.setFood(c2);
		} else {
			c.setStrength(c1.getStrength());
			c.setDefense(c1.getDefense());
			c.setMates(c1);
			c.setEnemies(c1);
			c.setFood(c1);
		}
		
		creatures.add(c);
		if (gameRunning) {
			repaint();
		}
	}

	public void flock() {
		float xCohesion = 0;
		float yCohesion = 0;

		float xSeperation = 0;
		float ySeperation = 0;

		float xAlignment = 0;
		float yAlignment = 0;

		float xGoal = 0;
		float yGoal = 0;

		for (int i = 0; i < creatures.size(); i++) {
			Creature c = (Creature) creatures.get(i);
			configureLists(c);
		}

		for (int i = 0; i < creatures.size(); i++) {
			Creature c = (Creature) creatures.get(i);
			if (c.isAlive()) {
				c.decreaseStrength();
				seeFriends(c);
				if (c.getNumberMates() > 0) {
					c.incrementMatingCycle();

					xCohesion = getCohesionX(c);
					yCohesion = getCohesionY(c);

					xSeperation = getSeperationX(c);
					ySeperation = getSeperationY(c);

					xAlignment = getAlignmentX(c);
					yAlignment = getAlignmentY(c);
				}

				if (seeEnemies(c) > 0) {
					fleeEnemies(c);
				}

				if (c.getStrength() <= lowStrength) {
					if (seePrey(c) > 0) {
						chasePrey(c);
					} else if (seeNutrient(c) > 0) {
						goToNutrient(c);
					}
				}
				
				
					c.dx = c.dx + xCohesion + xSeperation + xAlignment + xGoal;
					c.dy = c.dy + yCohesion + ySeperation + yAlignment + yGoal;
					repaint();
				
				limitVelocity(c);
				repaint();

				if (c.x < 1) {
					c.dx = BOUNCE;
				}
				if (c.x > (WORLD_WIDTH - 3)) {
					c.dx = -BOUNCE;
				}
				if (c.y < 1) {
					c.dy = BOUNCE;
				}
				if (c.y > (WORLD_HEIGTH - 1)) {
					c.dy = -BOUNCE;
				}

				c.x = c.x + c.dx;
				c.y = c.y + c.dy;
				
				if (gameRunning) {
					repaint();
				}

				if (c.getNearestPrey() != null) {
					eatPrey(c);
				}

				if (c.getNearestFood() != null) {
					eatFood(c);
				}

				if (c.getStrength() < 1) {
					c.setAlive(false);
				}

				c.increaseAge();
				if (c.getAge() >= c.getMaxAge()) {
					c.setAlive(false);
				}
				c.getFitness();
				creatures.set(i, c);
			} else {
				c.resetMatingCycle();
				float nutrientX = creatures.get(i).getX();
				float nutrientY = creatures.get(i).getY();
				nutrients.add(new Nutrient(nutrientX, nutrientY));
				creatures.remove(i);
			}
		}
	}

	public void configureLists(Creature cj) {
		// if (cj.isJustCreated()) {
		// cj.setJustCreated(false);
		for (int i = 0; i < creatures.size(); i++) {
			Creature c = (Creature) creatures.get(i);
			if (!c.equals(cj)) {
				if (cj.isFriend(c)) {
					if (!cj.containsInCommunity(c)) {
						cj.addToCommunity(c);
					}
				} else if (cj.isPredator(c)) {
					if (!cj.containsInEnemyList(c)) {
						cj.addToEnemyList(c);
					}
				} else if (cj.isPrey(c)) {
					if (!cj.containsInPreyList(c)) {
						cj.addToPreyList(c);
					}
				}
			}
		}

		/*int mateSize = cj.getMates().size();
		for (int i = 0; i < mateSize; i++) {
			Creature c = (Creature) cj.getMate(i);
			if (!creatures.contains(c)) {
				cj.removeMate(i);
			}
		}
		int enemySize = cj.getEnemies().size();
		for (int i = 0; i < enemySize; i++) {
			Creature c = (Creature) cj.getEnemy(i);
			if (!creatures.contains(c)) {
				cj.removeEnemy(i);
			}
		}
		int preySize = cj.getFood().size();
		for (int i = 0; i < preySize; i++) {
			Creature c = (Creature) cj.getFood(i);
			if (!creatures.contains(c)) {
				cj.removePrey(i);
			}
		}*/
		Iterator<Creature> mateiter = cj.getMates().iterator();
		while (mateiter.hasNext()) {
			if (!creatures.contains(mateiter.next())) {
				mateiter.remove();
			}
		}
		
		Iterator<Creature> enemyiter = cj.getEnemies().iterator();
		while (enemyiter.hasNext()) {
			if (!creatures.contains(enemyiter.next())) {
				enemyiter.remove();
			}
		}
		
		Iterator<Creature> preyiter = cj.getFood().iterator();
		while (preyiter.hasNext()) {
			if (!creatures.contains(preyiter.next())) {
				preyiter.remove();
			}
		}
	}

	public void goToNutrient(Creature c) {
		float nutrientX = 0.0f;
		float nutrientY = 0.0f;
		Nutrient n = c.getNearestFood();
		nutrientX = (c.getNearestFood().getX()) - c.getX();
		nutrientY = (c.getNearestFood().getY()) - c.getY();
		c.dx = c.dx + nutrientX;
		c.dy = c.dy + nutrientY;
		c.dx *= c.getMaxSpeed();
		c.dy *= c.getMaxSpeed();
		if (nutrients.contains(n)) {
			nutrients.remove(n);
		}
	}

	public void eatFood(Creature c) {
		if (c.getDistanceNearestFood() <= distanceEating) {
			c.increaseStrength();

			for (int i = 0; i < nutrients.size(); i++) {
				Nutrient n = (Nutrient) nutrients.get(i);
				if (n.equals(c.getNearestFood())) {
					nutrients.remove(i);
				}
			}

			c.setNumberFoodSeen(0);
			c.setNearestFood(null);
			c.setDistanceNearestFood(Float.POSITIVE_INFINITY);
		}
	}

	public void eatPrey(Creature c) {
		if (c.getDistanceNearestPrey() <= distanceEating) {
			c.increaseStrength();
			c.getNearestPrey().decreaseStrength();

			if (c.getNearestPrey().getStrength() < 1) {
				c.getNearestPrey().setAlive(false);
			}

			c.setNumberPreySeen(0);
			c.setNearestPrey(null);
			c.setDistanceNearestPrey(Float.POSITIVE_INFINITY);
		}
	}

	public void chasePrey(Creature c) {
		float PreyX = 0.0f;
		float PreyY = 0.0f;
		PreyX = (c.getNearestPrey().getX()) - c.getX();
		PreyY = (c.getNearestPrey().getY()) - c.getY();
		c.dx = c.dx + PreyX;
		c.dy = c.dy + PreyY;
		c.dx *= c.getMaxSpeed();
		c.dy *= c.getMaxSpeed();
	}

	public void fleeEnemies(Creature c) {
		float nearestPredatorX = 0.0f;
		float nearestPredatorY = 0.0f;
		if (c.getDistanceNearestEnemy() < distanceEnemy) {
			nearestPredatorX = c.getX() - (c.getNearestEnemy().getX());
			nearestPredatorY = c.getY() - (c.getNearestEnemy().getY());
			c.dx = c.dx + nearestPredatorX;
			c.dy = c.dy + nearestPredatorY;
		}
	}

	public void seeFriends(Creature cj) {
		/*
		 * float dist = 0.0f;
		 * 
		 * cj.clearMatesList();
		 * 
		 * for (int i = 0; i < creatures.size(); i++) { Creature c = (Creature)
		 * creatures.get(i); if (!cj.equals(c)) { dist = getDistance(cj, c); if
		 * (cj.getPerception() > dist) { if (cj.isFriend(c)) {
		 * cj.addToNeighboursList(c); cj.increaseDefense(10); if
		 * (!cj.containsInCommunity(c)) { cj.addToCommunity(c); } } if (dist <
		 * cj.getDistanceNearestMate()) { cj.setDistanceNearestMate(dist);
		 * cj.setNearestFlockMate(c); } } } }
		 */

		float dist = 0.0f;

		cj.clearMatesList();

		for (int i = 0; i < cj.getMates().size(); i++) {
			Creature c = (Creature) cj.getMate(i);

			dist = getDistance(cj, c);
			if (cj.getPerception() > dist) {
				cj.addToNeighboursList(c);
				cj.increaseDefense(10);
				if (dist < cj.getDistanceNearestMate()) {
					cj.setDistanceNearestMate(dist);
					cj.setNearestFlockMate(c);
				}
			}
		}
	}

	public int seeEnemies(Creature cj) {
		/*
		 * float dist = 0.0f;
		 * 
		 * cj.setNumberEnemiesSeen(0); cj.setNearestEnemy(null);
		 * cj.setDistanceNearestEnemy(Float.POSITIVE_INFINITY);
		 * 
		 * for (int i = 0; i < creatures.size(); i++) { Creature c = (Creature)
		 * creatures.get(i); if (!cj.equals(c)) { dist = getDistance(cj, c); if
		 * (cj.getPerception() > dist) { if (cj.isPredator(c)) {
		 * cj.increaseEnemiesSeen(); cj.decreaseDefense(10); if
		 * (!cj.containsInEnemyList(c)) { cj.addToEnemyList(c); } if (dist <
		 * cj.getDistanceNearestEnemy()) { cj.setDistanceNearestEnemy(dist);
		 * cj.setNearestEnemy(c); } } } } } return cj.getEnemiesSeen();
		 */

		float dist = 0.0f;

		cj.setNumberEnemiesSeen(0);
		cj.setNearestEnemy(null);
		cj.setDistanceNearestEnemy(Float.POSITIVE_INFINITY);

		for (int i = 0; i < cj.getEnemies().size(); i++) {
			Creature c = (Creature) cj.getEnemy(i);
			dist = getDistance(cj, c);
			if (cj.getPerception() > dist) {

				cj.increaseEnemiesSeen();
				cj.decreaseDefense(10);
				if (dist < cj.getDistanceNearestEnemy()) {
					cj.setDistanceNearestEnemy(dist);
					cj.setNearestEnemy(c);
				}
			}
		}
		return cj.getEnemiesSeen();
	}

	public int seePrey(Creature cj) {
		/*
		 * float dist = 0.0f; cj.setNumberPreySeen(0); cj.setNearestPrey(null);
		 * cj.setDistanceNearestPrey(Float.POSITIVE_INFINITY);
		 * 
		 * for (int i = 0; i < creatures.size(); i++) { Creature c = (Creature)
		 * creatures.get(i); if (!cj.equals(c)) { dist = getDistance(cj, c); if
		 * (cj.getPerception() > dist) { if (cj.isPrey(c)) {
		 * cj.increasePreySeen(); if (!cj.containsInPreyList(c)) {
		 * cj.addToPreyList(c); } if (dist < cj.getDistanceNearestPrey()) {
		 * cj.setDistanceNearestPrey(dist); cj.setNearestPrey(c); } } } } }
		 * return cj.getPreySeen();
		 */

		float dist = 0.0f;
		cj.setNumberPreySeen(0);
		cj.setNearestPrey(null);
		cj.setDistanceNearestPrey(Float.POSITIVE_INFINITY);

		for (int i = 0; i < cj.getFood().size(); i++) {
			Creature c = (Creature) cj.getFood(i);
			dist = getDistance(cj, c);
			if (cj.getPerception() > dist) {

				cj.increasePreySeen();

				if (dist < cj.getDistanceNearestPrey()) {
					cj.setDistanceNearestPrey(dist);
					cj.setNearestPrey(c);
				}
			}
		}
		return cj.getPreySeen();
	}

	public int seeNutrient(Creature cj) {
		float dist = 0.0f;

		cj.setNumberFoodSeen(0);
		cj.setNearestFood(null);
		cj.setDistanceNearestFood(Float.POSITIVE_INFINITY);

		for (int i = 0; i < nutrients.size(); i++) {
			Nutrient n = (Nutrient) nutrients.get(i);
			dist = getDistance(cj, n);
			if (cj.getPerception() > dist) {
				cj.increaseFoodSeen();
				if (dist < cj.getDistanceNearestFood()) {
					cj.setDistanceNearestFood(dist);
					cj.setNearestFood(n);
				}
			}
		}

		return cj.getNumFoodSeen();
	}

	public float getDistance(Creature cj, Creature c) {
		float dx = cj.getX() - c.getX();
		float dy = cj.getY() - c.getY();
		float dist = (float) Math.sqrt((dx * dx) + (dy * dy));
		return dist;
	}

	public float getDistance(Creature cj, Nutrient n) {
		float dx = cj.getX() - n.getX();
		float dy = cj.getY() - n.getY();
		float dist = (float) Math.sqrt((dx * dx) + (dy * dy));
		return dist;
	}

	public void limitVelocity(Creature cj) {

		if (cj.getSpeed() > cj.getMaxSpeed()) {

			cj.dx *= cj.getMaxSpeed();
			cj.dy *= cj.getMaxSpeed();
			cj.setSpeed(cj.getMaxSpeed());
		}
	}

	public float getAlignmentY(Creature cj) {
		float perceivedVelY = 0.0f;
		perceivedVelY = cj.getNearestMate().getDY();
		perceivedVelY *= 0.05f;
		return perceivedVelY;
	}

	public float getAlignmentX(Creature cj) {
		float perceivedVelX = 0.0f;
		perceivedVelX = cj.getNearestMate().getDX();
		perceivedVelX *= 0.05f;
		return perceivedVelX;
	}

	public float getSeperationY(Creature cj) {
		float ratio = cj.getDistanceNearestMate() / distanceMate;
		float displacementY = cj.getNearestMate().getY() - cj.getY();
		
		if (ratio < 0.05f) {
			ratio = 0.05f;
		}
		if (ratio > 0.1f) {
			ratio = 0.1f;
		}
		
		if (cj.getDistanceNearestMate() < distanceMate) {
			displacementY *= -ratio;
		}
		else if (cj.getDistanceNearestMate() < distanceMate) {
			displacementY *= ratio;
		}
		return displacementY;
	}

	public float getSeperationX(Creature cj) {
		float ratio = cj.getDistanceNearestMate() / distanceMate;
		float displacementX = cj.getNearestMate().getX() - cj.getX();

		if (ratio < 0.05f) {
			ratio = 0.05f;
		}
		if (ratio > 0.1f) {
			ratio = 0.1f;
		}
		
		if (cj.getDistanceNearestMate() < distanceMate) {
			displacementX *= -ratio;
		}
		else if (cj.getDistanceNearestMate() < distanceMate) {
			displacementX *= ratio;
		}
		return displacementX;
	}

	public float getCohesionY(Creature cj) {
		float perceivedCentralY = 0;

		for (int i = 0; i < cj.getNumberMates(); i++) {
			if (cj.getNeighbour(i) != null) {
				perceivedCentralY += cj.getNeighbour(i).getY();
			}
		}

		perceivedCentralY /= (cj.getNumberMates());

		perceivedCentralY = (perceivedCentralY - cj.getY());
		perceivedCentralY *= 0.05f;
		return perceivedCentralY;
	}

	public float getCohesionX(Creature cj) {
		float perceivedCentralX = 0.0f;

		for (int i = 0; i < cj.getNumberMates(); i++) {
			if (cj.getNeighbour(i) != null) {
				perceivedCentralX += cj.getNeighbour(i).getX();
			}
		}
		perceivedCentralX /= (cj.getNumberMates());

		perceivedCentralX = (perceivedCentralX - cj.getX());
		perceivedCentralX *= 0.05f;
		return perceivedCentralX;
	}

	public float getGoalX(Creature cj) {
		float xGoal = 100.0f;

		return (xGoal - cj.x) / 80000;
	}

	public float getGoalY(Creature cj) {
		float yGoal = 100.0f;

		return (yGoal - cj.y) / 80000;
	}

	public Creature selection() {
		float sumFitness = 0;

		for (int i = 0; i < creatures.size(); i++) {
			Creature c = creatures.get(i);
			int fit = c.getFitness();
			sumFitness += fit;
		}

		Random ran = new Random();
		float slice = sumFitness * ran.nextFloat();

		float fitnessSoFar = 0.0f;

		for (int i = 0; i < creatures.size(); i++) {
			Creature c = creatures.get(i);

			fitnessSoFar += c.getFitness();

			if (fitnessSoFar >= slice) {
				return c;
			}

		}

		return null;
	}

	@Override
	public void keyPressed(KeyEvent e) {
		int keycode = e.getKeyCode();

		if (keycode == KeyEvent.VK_ESCAPE) {
			System.exit(0);
		} else if (keycode == KeyEvent.VK_P) {
			if (gameRunning == true) {
				gameRunning = false;
			} else {
				gameRunning = true;
			}
		} else if (keycode == KeyEvent.VK_A) {
			if (SLEEP < 10) {
				SLEEP++;
			} else if (SLEEP < 150) {
				SLEEP += 10;
			}
		} else if (keycode == KeyEvent.VK_S) {
			if (SLEEP > 10) {
				SLEEP -= 10;
			} else if (SLEEP > 1) {
				SLEEP--;
			}
		}
		e.consume();
	}

	@Override
	public void keyReleased(KeyEvent e) {
		e.consume();
	}

	@Override
	public void keyTyped(KeyEvent e) {
		e.consume();
	}
}